Merge pull request #324 from dsander/omniauth

OAuth consumer

Dominik Sander 10 years ago
parent
commit
74ced66beb
50 changed files with 1077 additions and 132 deletions
  1. 14 0
      .env.example
  2. 1 1
      .travis.yml
  3. 5 0
      Gemfile
  4. 25 0
      Gemfile.lock
  5. 2 0
      app/assets/javascripts/application.js.coffee.erb
  6. 32 0
      app/concerns/oauthable.rb
  7. 6 4
      app/concerns/twitter_concern.rb
  8. 8 7
      app/controllers/agents_controller.rb
  9. 23 0
      app/controllers/application_controller.rb
  10. 2 0
      app/controllers/home_controller.rb
  11. 41 0
      app/controllers/services_controller.rb
  12. 5 0
      app/helpers/service_helper.rb
  13. 1 0
      app/models/agent.rb
  14. 33 36
      app/models/agents/basecamp_agent.rb
  15. 1 5
      app/models/agents/twitter_publish_agent.rb
  16. 1 5
      app/models/agents/twitter_stream_agent.rb
  17. 1 5
      app/models/agents/twitter_user_agent.rb
  18. 18 5
      app/models/scenario_import.rb
  19. 89 0
      app/models/service.rb
  20. 5 0
      app/models/user.rb
  21. 5 1
      app/views/agents/_form.html.erb
  22. 6 0
      app/views/agents/_oauth_dropdown.html.erb
  23. 26 0
      app/views/application/_upgrade_warning.html.erb
  24. 1 0
      app/views/layouts/_navigation.html.erb
  25. 4 1
      app/views/layouts/application.html.erb
  26. 11 0
      app/views/scenario_imports/_step_two.html.erb
  27. 58 0
      app/views/services/index.html.erb
  28. 5 0
      config/initializers/omniauth.rb
  29. 7 0
      config/routes.rb
  30. 18 0
      db/migrate/20140515211100_create_services.rb
  31. 5 0
      db/migrate/20140525150040_add_service_id_to_agents.rb
  32. 61 0
      db/migrate/20140525150140_migrate_agents_to_service_authentication.rb
  33. 5 0
      db/migrate/20140809211540_remove_service_index_on_user_id.rb
  34. 7 0
      db/migrate/20140811200922_add_uid_column_to_services.rb
  35. 57 34
      db/schema.rb
  36. 58 0
      spec/controllers/services_controller_spec.rb
  37. 43 0
      spec/data_fixtures/services/37signals.json
  38. 52 0
      spec/data_fixtures/services/github.json
  39. 66 0
      spec/data_fixtures/services/twitter.json
  40. 5 0
      spec/env.test
  41. 12 0
      spec/fixtures/agents.yml
  42. 17 0
      spec/fixtures/services.yml
  43. 11 26
      spec/models/agents/basecamp_agent_spec.rb
  44. 1 0
      spec/models/agents/twitter_publish_agent_spec.rb
  45. 1 0
      spec/models/agents/twitter_stream_agent_spec.rb
  46. 2 0
      spec/models/agents/twitter_user_agent_spec.rb
  47. 29 0
      spec/models/concerns/oauthable.rb
  48. 55 0
      spec/models/scenario_import_spec.rb
  49. 129 0
      spec/models/service_spec.rb
  50. 7 2
      spec/spec_helper.rb

+ 14 - 0
.env.example

@@ -70,6 +70,20 @@ EMAIL_FROM_ADDRESS=from_address@gmail.com
70 70
 # Number of lines of log messages to keep per Agent
71 71
 AGENT_LOG_LENGTH=200
72 72
 
73
+########################################################################################################
74
+#    OAuth Configuration                                                                               #
75
+#  More information at the wiki: https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications #
76
+########################################################################################################
77
+
78
+TWITTER_OAUTH_KEY=
79
+TWITTER_OAUTH_SECRET=
80
+
81
+THIRTY_SEVEN_SIGNALS_OAUTH_KEY=
82
+THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=
83
+
84
+GITHUB_OAUTH_KEY=
85
+GITHUB_OAUTH_SECRET=
86
+
73 87
 #############################
74 88
 #  AWS and Mechanical Turk  #
75 89
 #############################

+ 1 - 1
.travis.yml

@@ -8,7 +8,7 @@ rvm:
8 8
   - 2.1.1
9 9
   - 1.9.3
10 10
 before_install:
11
-  - travis_retry gem install bundler  
11
+  - travis_retry gem install bundler
12 12
 before_script:
13 13
   - mysql -e 'create database huginn_test;'
14 14
   - bundle exec rake db:migrate db:test:prepare

+ 5 - 0
Gemfile

@@ -88,6 +88,11 @@ gem 'slack-notifier', '~> 0.5.0'
88 88
 gem 'therubyracer', '~> 0.12.1'
89 89
 gem 'mqtt'
90 90
 
91
+gem 'omniauth'
92
+gem 'omniauth-twitter'
93
+gem 'omniauth-37signals'
94
+gem 'omniauth-github'
95
+
91 96
 group :development do
92 97
   gem 'binding_of_caller'
93 98
   gem 'better_errors'

+ 25 - 0
Gemfile.lock

@@ -191,12 +191,33 @@ GEM
191 191
     net-ftp-list (3.2.8)
192 192
     nokogiri (1.6.3.1)
193 193
       mini_portile (= 0.6.0)
194
+    oauth (0.4.7)
194 195
     oauth2 (0.9.4)
195 196
       faraday (>= 0.8, < 0.10)
196 197
       jwt (~> 1.0)
197 198
       multi_json (~> 1.3)
198 199
       multi_xml (~> 0.5)
199 200
       rack (~> 1.2)
201
+    omniauth (1.2.2)
202
+      hashie (>= 1.2, < 4)
203
+      rack (~> 1.0)
204
+    omniauth-37signals (1.0.5)
205
+      omniauth (~> 1.0)
206
+      omniauth-oauth2 (~> 1.0)
207
+    omniauth-github (1.1.2)
208
+      omniauth (~> 1.0)
209
+      omniauth-oauth2 (~> 1.1)
210
+    omniauth-oauth (1.0.1)
211
+      oauth
212
+      omniauth (~> 1.0)
213
+    omniauth-oauth2 (1.1.2)
214
+      faraday (>= 0.8, < 0.10)
215
+      multi_json (~> 1.3)
216
+      oauth2 (~> 0.9.3)
217
+      omniauth (~> 1.2)
218
+    omniauth-twitter (1.0.1)
219
+      multi_json (~> 1.3)
220
+      omniauth-oauth (~> 1.0)
200 221
     orm_adapter (0.5.0)
201 222
     pg (0.17.1)
202 223
     polyglot (0.3.5)
@@ -402,6 +423,10 @@ DEPENDENCIES
402 423
   mysql2 (~> 0.3.16)
403 424
   net-ftp-list (~> 3.2.8)
404 425
   nokogiri (~> 1.6.1)
426
+  omniauth
427
+  omniauth-37signals
428
+  omniauth-github
429
+  omniauth-twitter
405 430
   pg
406 431
   protected_attributes (~> 1.0.8)
407 432
   pry

+ 2 - 0
app/assets/javascripts/application.js.coffee.erb

@@ -169,6 +169,8 @@ $(document).ready ->
169 169
 
170 170
         $(".description").html(json.description_html) if json.description_html?
171 171
 
172
+        $('.oauthable-form').html(json.form) if json.form?
173
+
172 174
         if $("#agent_options").hasClass("showing-default") || $("#agent_options").val().match(/\A\s*(\{\s*\}|)\s*\Z/g)
173 175
           window.jsonEditor.json = json.options
174 176
           window.jsonEditor.rebuild()

+ 32 - 0
app/concerns/oauthable.rb

@@ -0,0 +1,32 @@
1
+module Oauthable
2
+  extend ActiveSupport::Concern
3
+
4
+  included do |base|
5
+    @valid_oauth_providers = :all
6
+    attr_accessible :service_id
7
+    validates_presence_of :service_id
8
+  end
9
+
10
+  def oauthable?
11
+    true
12
+  end
13
+
14
+  def valid_services_for(user)
15
+    if valid_oauth_providers == :all
16
+      user.available_services
17
+    else
18
+      user.available_services.where(provider: valid_oauth_providers)
19
+    end
20
+  end
21
+
22
+  def valid_oauth_providers
23
+    self.class.valid_oauth_providers
24
+  end
25
+
26
+  module ClassMethods
27
+    def valid_oauth_providers(*providers)
28
+      return @valid_oauth_providers if providers == []
29
+      @valid_oauth_providers = providers
30
+    end
31
+  end
32
+end

+ 6 - 4
app/concerns/twitter_concern.rb

@@ -1,8 +1,10 @@
1 1
 module TwitterConcern
2 2
   extend ActiveSupport::Concern
3
+  include Oauthable
3 4
 
4 5
   included do
5 6
     validate :validate_twitter_options
7
+    valid_oauth_providers :twitter
6 8
   end
7 9
 
8 10
   def validate_twitter_options
@@ -15,19 +17,19 @@ module TwitterConcern
15 17
   end
16 18
 
17 19
   def twitter_consumer_key
18
-    options['consumer_key'].presence || credential('twitter_consumer_key')
20
+    ENV['TWITTER_OAUTH_KEY']
19 21
   end
20 22
 
21 23
   def twitter_consumer_secret
22
-    options['consumer_secret'].presence || credential('twitter_consumer_secret')
24
+    ENV['TWITTER_OAUTH_SECRET']
23 25
   end
24 26
 
25 27
   def twitter_oauth_token
26
-    options['oauth_token'].presence || options['access_key'].presence || credential('twitter_oauth_token')
28
+    service.token
27 29
   end
28 30
 
29 31
   def twitter_oauth_token_secret
30
-    options['oauth_token_secret'].presence || options['access_secret'].presence || credential('twitter_oauth_token_secret')
32
+    service.secret
31 33
   end
32 34
 
33 35
   def twitter

+ 8 - 7
app/controllers/agents_controller.rb

@@ -31,14 +31,15 @@ class AgentsController < ApplicationController
31 31
   end
32 32
 
33 33
   def type_details
34
-    agent = Agent.build_for_type(params[:type], current_user, {})
34
+    @agent = Agent.build_for_type(params[:type], current_user, {})
35 35
     render :json => {
36
-        :can_be_scheduled => agent.can_be_scheduled?,
37
-        :default_schedule => agent.default_schedule,
38
-        :can_receive_events => agent.can_receive_events?,
39
-        :can_create_events => agent.can_create_events?,
40
-        :options => agent.default_options,
41
-        :description_html => agent.html_description
36
+        :can_be_scheduled => @agent.can_be_scheduled?,
37
+        :default_schedule => @agent.default_schedule,
38
+        :can_receive_events => @agent.can_receive_events?,
39
+        :can_create_events => @agent.can_create_events?,
40
+        :options => @agent.default_options,
41
+        :description_html => @agent.html_description,
42
+        :form => render_to_string(partial: 'oauth_dropdown')
42 43
     }
43 44
   end
44 45
 

+ 23 - 0
app/controllers/application_controller.rb

@@ -13,4 +13,27 @@ class ApplicationController < ActionController::Base
13 13
     devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :username, :email, :password, :remember_me) }
14 14
     devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:username, :email, :password, :password_confirmation, :current_password) }
15 15
   end
16
+
17
+  def upgrade_warning
18
+    return unless current_user
19
+    twitter_oauth_check
20
+    basecamp_auth_check
21
+  end
22
+
23
+  private
24
+  def twitter_oauth_check
25
+    if ENV['TWITTER_OAUTH_KEY'].blank? || ENV['TWITTER_OAUTH_SECRET'].blank?
26
+      if @twitter_agent = current_user.agents.where("type like 'Agents::Twitter%'").first
27
+        @twitter_oauth_key    = @twitter_agent.options['consumer_key'].presence || @twitter_agent.credential('twitter_consumer_key')
28
+        @twitter_oauth_secret = @twitter_agent.options['consumer_secret'].presence || @twitter_agent.credential('twitter_consumer_secret')
29
+      end
30
+    end
31
+  end
32
+
33
+  def basecamp_auth_check
34
+    if ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'].blank? || ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'].blank?
35
+      @basecamp_agent = current_user.agents.where(type: 'Agents::BasecampAgent').first
36
+    end
37
+  end
38
+
16 39
 end

+ 2 - 0
app/controllers/home_controller.rb

@@ -1,6 +1,8 @@
1 1
 class HomeController < ApplicationController
2 2
   skip_before_filter :authenticate_user!
3 3
 
4
+  before_filter :upgrade_warning, only: :index
5
+
4 6
   def index
5 7
   end
6 8
 

+ 41 - 0
app/controllers/services_controller.rb

@@ -0,0 +1,41 @@
1
+class ServicesController < ApplicationController
2
+  before_filter :upgrade_warning, only: :index
3
+
4
+  def index
5
+    @services = current_user.services.page(params[:page])
6
+
7
+    respond_to do |format|
8
+      format.html
9
+      format.json { render json: @services }
10
+    end
11
+  end
12
+
13
+  def destroy
14
+    @services = current_user.services.find(params[:id])
15
+    @services.destroy
16
+
17
+    respond_to do |format|
18
+      format.html { redirect_to services_path }
19
+      format.json { head :no_content }
20
+    end
21
+  end
22
+
23
+  def toggle_availability
24
+    @service = current_user.services.find(params[:id])
25
+    @service.toggle_availability!
26
+
27
+    respond_to do |format|
28
+      format.html { redirect_to services_path }
29
+      format.json { render json: @service }
30
+    end
31
+  end
32
+
33
+  def callback
34
+    @service = current_user.services.initialize_or_update_via_omniauth(request.env['omniauth.auth'])
35
+    if @service && @service.save
36
+      redirect_to services_path, notice: "The service was successfully created."
37
+    else
38
+      redirect_to services_path, error: "Error creating the service."
39
+    end
40
+  end
41
+end

+ 5 - 0
app/helpers/service_helper.rb

@@ -0,0 +1,5 @@
1
+module ServiceHelper
2
+  def has_oauth_configuration_for(provider)
3
+    ENV["#{provider.upcase}_OAUTH_KEY"].present? && ENV["#{provider.upcase}_OAUTH_SECRET"].present?
4
+  end
5
+end

+ 1 - 0
app/models/agent.rb

@@ -44,6 +44,7 @@ class Agent < ActiveRecord::Base
44 44
   after_save :possibly_update_event_expirations
45 45
 
46 46
   belongs_to :user, :inverse_of => :agents
47
+  belongs_to :service, :inverse_of => :agents
47 48
   has_many :events, -> { order("events.id desc") }, :dependent => :delete_all, :inverse_of => :agent
48 49
   has_one  :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc"
49 50
   has_many :logs,  -> { order("agent_logs.id desc") }, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog"

+ 33 - 36
app/models/agents/basecamp_agent.rb

@@ -2,17 +2,18 @@ module Agents
2 2
   class BasecampAgent < Agent
3 3
     cannot_receive_events!
4 4
 
5
+    include Oauthable
6
+    valid_oauth_providers '37signals'
7
+
5 8
     description <<-MD
6 9
       The BasecampAgent checks a Basecamp project for new Events
7 10
 
8
-      It is required that you enter your Basecamp credentials (`username` and `password`).
11
+      To be able to use this Agent you need to authenticate with 37signals in the [Services](/services) section first.
9 12
 
10
-      You also need to provide your Basecamp `user_id` and the `project_id` of the project you want to monitor.
13
+      You need to provide the `project_id` of the project you want to monitor.
11 14
       If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows:
12 15
 
13
-      `https://basecamp.com/`
14
-      user_id
15
-      `/projects/`
16
+      `https://basecamp.com/123456/projects/`
16 17
       project_id
17 18
       `-explore-basecamp`
18 19
     MD
@@ -20,42 +21,36 @@ module Agents
20 21
     event_description <<-MD
21 22
       Events are the raw JSON provided by the Basecamp API. Should look something like:
22 23
 
23
-        {
24
-          "creator": {
25
-            "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3",
26
-            "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3",
27
-            "name": "Dominik Sander",
28
-            "id": 123456
29
-          },
30
-          "attachments": [],
31
-          "raw_excerpt": "test test",
32
-          "excerpt": "test test",
33
-          "id": 6454342343,
34
-          "created_at": "2014-04-17T10:25:31.000+02:00",
35
-          "updated_at": "2014-04-17T10:25:31.000+02:00",
36
-          "summary": "commented on whaat",
37
-          "action": "commented on",
38
-          "target": "whaat",
39
-          "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json",
40
-          "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545"
41
-        }
24
+          {
25
+            "creator": {
26
+              "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3",
27
+              "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3",
28
+              "name": "Dominik Sander",
29
+              "id": 123456
30
+            },
31
+            "attachments": [],
32
+            "raw_excerpt": "test test",
33
+            "excerpt": "test test",
34
+            "id": 6454342343,
35
+            "created_at": "2014-04-17T10:25:31.000+02:00",
36
+            "updated_at": "2014-04-17T10:25:31.000+02:00",
37
+            "summary": "commented on whaat",
38
+            "action": "commented on",
39
+            "target": "whaat",
40
+            "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json",
41
+            "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545"
42
+          }
42 43
     MD
43 44
 
44 45
     default_schedule "every_10m"
45 46
 
46 47
     def default_options
47 48
       {
48
-        'username' => '',
49
-        'password' => '',
50
-        'user_id' => '',
51 49
         'project_id' => '',
52 50
       }
53 51
     end
54 52
 
55 53
     def validate_options
56
-      errors.add(:base, "you need to specify your basecamp username") unless options['username'].present?
57
-      errors.add(:base, "you need to specify your basecamp password") unless options['password'].present?
58
-      errors.add(:base, "you need to specify your basecamp user id") unless options['user_id'].present?
59 54
       errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present?
60 55
     end
61 56
 
@@ -64,27 +59,29 @@ module Agents
64 59
     end
65 60
 
66 61
     def check
62
+      service.prepare_request
67 63
       reponse = HTTParty.get request_url, request_options.merge(query_parameters)
68
-      memory[:last_run] = Time.now.utc.iso8601
69
-      if last_check_at != nil
70
-        JSON.parse(reponse.body).each do |event|
64
+      events = JSON.parse(reponse.body)
65
+      if !memory[:last_event].nil?
66
+        events.each do |event|
71 67
           create_event :payload => event
72 68
         end
73 69
       end
70
+      memory[:last_event] = events.first['created_at'] if events.length > 0
74 71
       save!
75 72
     end
76 73
 
77 74
   private
78 75
     def request_url
79
-      "https://basecamp.com/#{URI.encode(interpolated[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json"
76
+      "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json"
80 77
     end
81 78
 
82 79
     def request_options
83
-      {:basic_auth => {:username => interpolated[:username], :password => interpolated[:password]}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}}
80
+      {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => "Bearer \"#{service.token}\""}}
84 81
     end
85 82
 
86 83
     def query_parameters
87
-      memory[:last_run].present? ? { :query => {:since => memory[:last_run]} } : {}
84
+      memory[:last_event].present? ? { :query => {:since => memory[:last_event]} } : {}
88 85
     end
89 86
   end
90 87
 end

+ 1 - 5
app/models/agents/twitter_publish_agent.rb

@@ -9,11 +9,7 @@ module Agents
9 9
     description <<-MD
10 10
       The TwitterPublishAgent publishes tweets from the events it receives.
11 11
 
12
-      Twitter credentials must be supplied as either [credentials](/user_credentials) called
13
-      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
14
-      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
15
-
16
-      To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
12
+      To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first.
17 13
 
18 14
       You must also specify a `message` parameter, you can use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the message.
19 15
 

+ 1 - 5
app/models/agents/twitter_stream_agent.rb

@@ -10,11 +10,7 @@ module Agents
10 10
       To follow the Twitter stream, provide an array of `filters`.  Multiple words in a filter must all show up in a tweet, but are independent of order.
11 11
       If you provide an array instead of a filter, the first entry will be considered primary and any additional values will be treated as aliases.
12 12
 
13
-      Twitter credentials must be supplied as either [credentials](/user_credentials) called
14
-      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
15
-      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
16
-
17
-      To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
13
+      To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first.
18 14
 
19 15
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
20 16
 

+ 1 - 5
app/models/agents/twitter_user_agent.rb

@@ -9,11 +9,7 @@ module Agents
9 9
     description <<-MD
10 10
       The TwitterUserAgent follows the timeline of a specified Twitter user.
11 11
 
12
-      Twitter credentials must be supplied as either [credentials](/user_credentials) called
13
-      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
14
-      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
15
-
16
-      To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
12
+      To be able to use this Agent you need to authenticate with Twitter in the [Services](/services) section first.
17 13
 
18 14
       You must also provide the `username` of the Twitter user to monitor.
19 15
 

+ 18 - 5
app/models/scenario_import.rb

@@ -80,17 +80,19 @@ class ScenarioImport
80 80
         agent.schedule = agent_diff.schedule.updated if agent_diff.schedule.present?
81 81
         agent.keep_events_for = agent_diff.keep_events_for.updated if agent_diff.keep_events_for.present?
82 82
         agent.propagate_immediately = agent_diff.propagate_immediately.updated if agent_diff.propagate_immediately.present? # == "true"
83
+        agent.service_id = agent_diff.service_id.updated if agent_diff.service_id.present?
83 84
         unless agent.save
84 85
           success = false
85 86
           errors.add(:base, "Errors when saving '#{agent_diff.name.incoming}': #{agent.errors.full_messages.to_sentence}")
86 87
         end
87 88
         agent
88 89
       end
89
-
90
-      links.each do |link|
91
-        receiver = created_agents[link['receiver']]
92
-        source = created_agents[link['source']]
93
-        receiver.sources << source unless receiver.sources.include?(source)
90
+      if success
91
+        links.each do |link|
92
+          receiver = created_agents[link['receiver']]
93
+          source = created_agents[link['source']]
94
+          receiver.sources << source unless receiver.sources.include?(source)
95
+        end
94 96
       end
95 97
     end
96 98
 
@@ -153,6 +155,9 @@ class ScenarioImport
153 155
           errors.add(:base, "Your updated options for '#{agent_data['name']}' were unparsable.")
154 156
         end
155 157
       end
158
+      if agent_diff.requires_service? && merges.present? && merges[index.to_s].present? && merges[index.to_s]['service_id'].present?
159
+        agent_diff.service_id = AgentDiff::FieldDiff.new(merges[index.to_s]['service_id'].to_i)
160
+      end
156 161
       agent_diff
157 162
     end
158 163
   end
@@ -196,6 +201,10 @@ class ScenarioImport
196 201
       @requires_merge
197 202
     end
198 203
 
204
+    def requires_service?
205
+      !!agent_instance.try(:oauthable?)
206
+    end
207
+
199 208
     def store!(agent_data)
200 209
       self.type = FieldDiff.new(agent_data["type"].split("::").pop)
201 210
       self.options = FieldDiff.new(agent_data['options'] || {})
@@ -256,5 +265,9 @@ class ScenarioImport
256 265
         key.gsub(/[^a-zA-Z0-9_-]/, '')
257 266
       end
258 267
     end
268
+
269
+    def agent_instance
270
+      "Agents::#{self.type.updated}".constantize.new
271
+    end
259 272
   end
260 273
 end

+ 89 - 0
app/models/service.rb

@@ -0,0 +1,89 @@
1
+class Service < ActiveRecord::Base
2
+  PROVIDER_TO_ENV_MAP = {'37signals' => 'THIRTY_SEVEN_SIGNALS'}
3
+
4
+  attr_accessible :provider, :name, :token, :secret, :refresh_token, :expires_at, :global, :options, :uid
5
+
6
+  serialize :options, Hash
7
+
8
+  belongs_to :user, :inverse_of => :services
9
+  has_many :agents, :inverse_of => :service
10
+
11
+  validates_presence_of :user_id, :provider, :name, :token
12
+
13
+  before_destroy :disable_agents
14
+
15
+  scope :available_to_user, lambda { |user| where("services.user_id = ? or services.global = true", user.id) }
16
+  scope :by_name, lambda { |dir = 'desc'| order("services.name #{dir}") }
17
+
18
+  def disable_agents(conditions = {})
19
+    agents.where.not(conditions[:where_not] || {}).each do |agent|
20
+      agent.service_id = nil
21
+      agent.disabled = true
22
+      agent.save!(validate: false)
23
+    end
24
+  end
25
+
26
+  def toggle_availability!
27
+    disable_agents(where_not: {user_id: self.user_id}) if global
28
+    self.global = !self.global
29
+    self.save!
30
+  end
31
+
32
+  def prepare_request
33
+    if expires_at && Time.now > expires_at
34
+      refresh_token!
35
+    end
36
+  end
37
+
38
+  def refresh_token!
39
+    response = HTTParty.post(endpoint, query: {
40
+                  type:          'refresh',
41
+                  client_id:     oauth_key,
42
+                  client_secret: oauth_secret,
43
+                  refresh_token: refresh_token
44
+    })
45
+    data = JSON.parse(response.body)
46
+    update(expires_at: Time.now + data['expires_in'], token: data['access_token'], refresh_token: data['refresh_token'].presence || refresh_token)
47
+  end
48
+
49
+  def endpoint
50
+    client_options = "OmniAuth::Strategies::#{OmniAuth::Utils.camelize(self.provider)}".constantize.default_options['client_options']
51
+    URI.join(client_options['site'], client_options['token_url'])
52
+  end
53
+
54
+  def provider_to_env
55
+    PROVIDER_TO_ENV_MAP[provider].presence || provider.upcase
56
+  end
57
+
58
+  def oauth_key
59
+    ENV["#{provider_to_env}_OAUTH_KEY"]
60
+  end
61
+
62
+  def oauth_secret
63
+    ENV["#{provider_to_env}_OAUTH_SECRET"]
64
+  end
65
+
66
+  def self.provider_specific_options(omniauth)
67
+    case omniauth['provider']
68
+      when 'twitter', 'github'
69
+        { name: omniauth['info']['nickname'] }
70
+      when '37signals'
71
+        { user_id: omniauth['extra']['accounts'][0]['id'], name: omniauth['info']['name'] }
72
+      else
73
+        { name: omniauth['info']['nickname'] }
74
+    end
75
+  end
76
+
77
+  def self.initialize_or_update_via_omniauth(omniauth)
78
+    options = provider_specific_options(omniauth)
79
+
80
+    find_or_initialize_by(provider: omniauth['provider'], uid: omniauth['uid'].to_s).tap do |service|
81
+      service.assign_attributes token: omniauth['credentials']['token'],
82
+                                secret: omniauth['credentials']['secret'],
83
+                                name: options[:name],
84
+                                refresh_token: omniauth['credentials']['refresh_token'],
85
+                                expires_at: omniauth['credentials']['expires_at'] && Time.at(omniauth['credentials']['expires_at']),
86
+                                options: options
87
+    end
88
+  end
89
+end

+ 5 - 0
app/models/user.rb

@@ -27,6 +27,11 @@ class User < ActiveRecord::Base
27 27
   has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user
28 28
   has_many :logs, :through => :agents, :class_name => "AgentLog"
29 29
   has_many :scenarios, :inverse_of => :user, :dependent => :destroy
30
+  has_many :services, -> { by_name('asc') }, :dependent => :destroy
31
+
32
+  def available_services
33
+    Service.available_to_user(self).by_name
34
+  end
30 35
 
31 36
   # Allow users to login via either email or username.
32 37
   def self.find_first_by_auth_conditions(warden_conditions)

+ 5 - 1
app/views/agents/_form.html.erb

@@ -25,11 +25,15 @@
25 25
             </div>
26 26
           <% end %>
27 27
 
28
-          <div class="form-group">
28
+          <div class="form-group type-select">
29 29
             <%= f.label :name %>
30 30
             <%= f.text_field :name, :class => 'form-control' %>
31 31
           </div>
32 32
 
33
+          <div class='oauthable-form'>
34
+            <%= render partial: 'oauth_dropdown' %>
35
+          </div>
36
+
33 37
           <div class="form-group">
34 38
             <%= f.label :schedule, :class => 'control-label' %>
35 39
             <div class="schedule-region" data-can-be-scheduled="<%= @agent.can_be_scheduled? %>">

+ 6 - 0
app/views/agents/_oauth_dropdown.html.erb

@@ -0,0 +1,6 @@
1
+<% if @agent.try(:oauthable?) %>
2
+  <div class="form-group type-select">
3
+    <%= label_tag :service %>
4
+    <%= select_tag 'agent[service_id]', options_for_select(@agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, @agent.service_id), class: 'form-control' %>
5
+  </div>
6
+<% end %>

+ 26 - 0
app/views/application/_upgrade_warning.html.erb

@@ -0,0 +1,26 @@
1
+<% if @twitter_agent || @basecamp_agent %>
2
+  <div class="alert alert-danger" role="alert">
3
+    <p>
4
+      <b>Warning!</b> You need to update your Huginn configuration, so your agents continue to work with the new OAuth services.
5
+    </p>
6
+    <br/>
7
+    <% if @twitter_agent %>
8
+      <p>
9
+        To complete the migration of your <b>Twitter</b> agents you need to update your .env file and add the following two lines:
10
+
11
+        <pre>
12
+TWITTER_OAUTH_KEY=<%= @twitter_oauth_key %>
13
+TWITTER_OAUTH_SECRET=<%= @twitter_oauth_secret %>
14
+        </pre>
15
+        To authenticate new accounts with your twitter OAuth application you need to log in the to <a href="https://apps.twitter.com/" target="_blank">twitter application management page</a> and set the callback URL of your application to "http<%= ENV['FORCE_SSL'] == 'true' ? 's' : '' %>://<%= ENV['DOMAIN'] %>/auth/twitter/callback".
16
+
17
+      </p>
18
+    <% end %>
19
+    <% if @basecamp_agent %>
20
+      <p>
21
+        Your <b>Basecamp</b> agents could not be migrated automatically. You need to manually register an application with 37signals and authenticate Huginn to use it.<br/>
22
+        Have a look at the <%= link_to 'Wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: '_blank' %> if you need help.
23
+      </p>
24
+    <% end %>
25
+  </div>
26
+<% end -%>

+ 1 - 0
app/views/layouts/_navigation.html.erb

@@ -22,6 +22,7 @@
22 22
       <%= nav_link "Scenarios", scenarios_path %>
23 23
       <%= nav_link "Events", events_path %>
24 24
       <%= nav_link "Credentials", user_credentials_path %>
25
+      <%= nav_link "Services", services_path %>
25 26
     </ul>
26 27
   <% end %>
27 28
   

+ 4 - 1
app/views/layouts/application.html.erb

@@ -24,7 +24,10 @@
24 24
           <%= render 'layouts/messages' %>
25 25
         </div>
26 26
       </div>
27
-      
27
+      <% if user_signed_in? %>
28
+        <%= render "upgrade_warning" %>
29
+      <% end %>
30
+
28 31
       <%= yield %>
29 32
       
30 33
     </div>

+ 11 - 0
app/views/scenario_imports/_step_two.html.erb

@@ -119,6 +119,17 @@
119 119
           </div>
120 120
         <% end %>
121 121
       </div>
122
+
123
+      <% if agent_diff.requires_service? %>
124
+        <div class='row'>
125
+          <div class='col-md-4'>
126
+            <div class="form-group type-select">
127
+              <%= label_tag "scenario_import[merges][#{index}][service_id]", 'Service' %>
128
+              <%= select_tag "scenario_import[merges][#{index}][service_id]", options_for_select(agent_diff.agent_instance.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent_diff.agent.try(:service_id)), class: 'form-control' %>
129
+            </div>
130
+          </div>
131
+        </div>
132
+      <% end %>
122 133
     </div>
123 134
   <% end %>
124 135
 </div>

+ 58 - 0
app/views/services/index.html.erb

@@ -0,0 +1,58 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Your Services
7
+        </h2>
8
+      </div>
9
+      <p>
10
+        Before you can authenticate with a service, you need to set it up. Have a look at the Huginn
11
+        <%= link_to 'wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: :_blank %>
12
+        for guidance.
13
+      </p>
14
+      <% if has_oauth_configuration_for('twitter') %>
15
+        <p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p>
16
+      <% end %>
17
+      <% if has_oauth_configuration_for('thirty_seven_signals') %>
18
+        <p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p>
19
+      <% end -%>
20
+      <% if has_oauth_configuration_for('github') %>
21
+        <p><%= link_to "Authenticate with Github", "/auth/github" %></p>
22
+      <% end -%>
23
+      <hr>
24
+
25
+      <div class='table-responsive'>
26
+        <table class='table table-striped events'>
27
+          <tr>
28
+            <th>Provider</th>
29
+            <th>Username</th>
30
+            <th>Global?</th>
31
+            <th></th>
32
+          </tr>
33
+
34
+        <% @services.each do |service| %>
35
+          <tr>
36
+            <td><%= service.provider %></td>
37
+            <td><%= service.name %></td>
38
+            <td><%= service.global ? 'Yes' : 'No' %></td>
39
+            <td>
40
+              <div class="btn-group btn-group-xs">
41
+                <% if service.global %>
42
+                  <%= link_to 'Make private', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to remove access to your data on this service for other users?'}, class: "btn btn-default" %>
43
+                <% else %>
44
+                   <%= link_to 'Make global', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to grant every user on this system access to your data on this service?'}, class: "btn btn-default" %>
45
+                <% end %>
46
+                <%= link_to 'Delete', service_path(service), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default btn-danger" %>
47
+              </div>
48
+            </td>
49
+          </tr>
50
+        <% end %>
51
+        </table>
52
+      </div>
53
+
54
+      <%= paginate @services, :theme => 'twitter-bootstrap-3' %>
55
+    </div>
56
+  </div>
57
+</div>
58
+

+ 5 - 0
config/initializers/omniauth.rb

@@ -0,0 +1,5 @@
1
+Rails.application.config.middleware.use OmniAuth::Builder do
2
+  provider :twitter, ENV['TWITTER_OAUTH_KEY'], ENV['TWITTER_OAUTH_SECRET'], authorize_params: {force_login: 'true', use_authorize: 'true'}
3
+  provider '37signals', ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'], ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET']
4
+  provider :github, ENV['GITHUB_OAUTH_KEY'], ENV['GITHUB_OAUTH_SECRET']
5
+end

+ 7 - 0
config/routes.rb

@@ -45,6 +45,12 @@ Huginn::Application.routes.draw do
45 45
 
46 46
   resources :user_credentials, :except => :show
47 47
 
48
+  resources :services, :only => [:index, :destroy] do
49
+    member do
50
+      post :toggle_availability
51
+    end
52
+  end
53
+
48 54
   get "/worker_status" => "worker_status#show"
49 55
 
50 56
   post "/users/:user_id/update_location/:secret" => "user_location_updates#create"
@@ -56,6 +62,7 @@ Huginn::Application.routes.draw do
56 62
 #  get "/delayed_job" => DelayedJobWeb, :anchor => false
57 63
 
58 64
   devise_for :users, :sign_out_via => [ :post, :delete ]
65
+  get '/auth/:provider/callback', to: 'services#callback'
59 66
 
60 67
   get "/about" => "home#about"
61 68
   root :to => "home#index"

+ 18 - 0
db/migrate/20140515211100_create_services.rb

@@ -0,0 +1,18 @@
1
+class CreateServices < ActiveRecord::Migration
2
+  def change
3
+    create_table :services do |t|
4
+      t.integer :user_id, null: false
5
+      t.string :provider, null: false
6
+      t.string :name, null: false
7
+      t.text :token, null: false
8
+      t.text :secret
9
+      t.text :refresh_token
10
+      t.datetime :expires_at
11
+      t.boolean :global, default: false
12
+      t.text :options
13
+      t.timestamps
14
+    end
15
+    add_index :services, :user_id
16
+    add_index :services, [:user_id, :global]
17
+  end
18
+end

+ 5 - 0
db/migrate/20140525150040_add_service_id_to_agents.rb

@@ -0,0 +1,5 @@
1
+class AddServiceIdToAgents < ActiveRecord::Migration
2
+  def change
3
+    add_column :agents, :service_id, :integer
4
+  end
5
+end

+ 61 - 0
db/migrate/20140525150140_migrate_agents_to_service_authentication.rb

@@ -0,0 +1,61 @@
1
+class MigrateAgentsToServiceAuthentication < ActiveRecord::Migration
2
+  def twitter_consumer_key(agent)
3
+    agent.options['consumer_key'].presence || agent.credential('twitter_consumer_key')
4
+  end
5
+
6
+  def twitter_consumer_secret(agent)
7
+    agent.options['consumer_secret'].presence || agent.credential('twitter_consumer_secret')
8
+  end
9
+
10
+  def twitter_oauth_token(agent)
11
+    agent.options['oauth_token'].presence || agent.options['access_key'].presence || agent.credential('twitter_oauth_token')
12
+  end
13
+
14
+  def twitter_oauth_token_secret(agent)
15
+    agent.options['oauth_token_secret'].presence || agent.options['access_secret'].presence || agent.credential('twitter_oauth_token_secret')
16
+  end
17
+
18
+  def up
19
+    agents = Agent.where(type: ['Agents::TwitterUserAgent', 'Agents::TwitterStreamAgent', 'Agents::TwitterPublishAgent']).each do |agent|
20
+      service = agent.user.services.create!(
21
+        provider: 'twitter',
22
+        name: "Migrated '#{agent.name}'",
23
+        token: twitter_oauth_token(agent),
24
+        secret: twitter_oauth_token_secret(agent)
25
+      )
26
+      agent.service_id = service.id
27
+      agent.save!(validate: false)
28
+    end
29
+    migrated = false
30
+    if agents.length > 0
31
+      puts <<-EOF.strip_heredoc
32
+
33
+        Your Twitter agents were successfully migrated. You need to update your .env file and add the following two lines:
34
+
35
+        TWITTER_OAUTH_KEY=#{twitter_consumer_key(agents.first)}
36
+        TWITTER_OAUTH_SECRET=#{twitter_consumer_secret(agents.first)}
37
+
38
+        To authenticate new accounts with your twitter OAuth application you need to log in the to twitter application management page (https://apps.twitter.com/)
39
+        and set the callback URL of your application to "http#{ENV['FORCE_SSL'] == 'true' ? 's' : ''}://#{ENV['DOMAIN']}/auth/twitter/callback"
40
+
41
+      EOF
42
+      migrated = true
43
+    end
44
+    if Agent.where(type: ['Agents::BasecampAgent']).count > 0
45
+      puts <<-EOF.strip_heredoc
46
+
47
+        Your Basecamp agents can not be migrated automatically. You need to manually register an application with 37signals and authenticate Huginn to use it.
48
+        Have a look at the wiki (https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications) if you need help.
49
+
50
+
51
+      EOF
52
+      migrated = true
53
+    end
54
+    sleep 20 if migrated
55
+  end
56
+
57
+  def down
58
+    raise ActiveRecord::IrreversibleMigration, "Cannot revert migration to OAuth services"
59
+  end
60
+end
61
+

+ 5 - 0
db/migrate/20140809211540_remove_service_index_on_user_id.rb

@@ -0,0 +1,5 @@
1
+class RemoveServiceIndexOnUserId < ActiveRecord::Migration
2
+  def change
3
+    remove_index :services, :user_id
4
+  end
5
+end

+ 7 - 0
db/migrate/20140811200922_add_uid_column_to_services.rb

@@ -0,0 +1,7 @@
1
+class AddUidColumnToServices < ActiveRecord::Migration
2
+  def change
3
+    add_column :services, :uid, :string
4
+    add_index :services, :uid
5
+    add_index :services, :provider
6
+  end
7
+end

+ 57 - 34
db/schema.rb

@@ -13,19 +13,22 @@
13 13
 
14 14
 ActiveRecord::Schema.define(version: 20140820003139) do
15 15
 
16
+  # These are extensions that must be enabled in order to support this database
17
+  enable_extension "plpgsql"
18
+
16 19
   create_table "agent_logs", force: true do |t|
17
-    t.integer  "agent_id",                                       null: false
18
-    t.text     "message",           limit: 16777215,             null: false
19
-    t.integer  "level",                              default: 3, null: false
20
+    t.integer  "agent_id",                      null: false
21
+    t.text     "message",                       null: false
22
+    t.integer  "level",             default: 3, null: false
20 23
     t.integer  "inbound_event_id"
21 24
     t.integer  "outbound_event_id"
22
-    t.datetime "created_at",                                     null: false
23
-    t.datetime "updated_at",                                     null: false
25
+    t.datetime "created_at"
26
+    t.datetime "updated_at"
24 27
   end
25 28
 
26 29
   create_table "agents", force: true do |t|
27 30
     t.integer  "user_id"
28
-    t.text     "options",               limit: 16777215
31
+    t.text     "options"
29 32
     t.string   "type"
30 33
     t.string   "name"
31 34
     t.string   "schedule"
@@ -33,16 +36,17 @@ ActiveRecord::Schema.define(version: 20140820003139) do
33 36
     t.datetime "last_check_at"
34 37
     t.datetime "last_receive_at"
35 38
     t.integer  "last_checked_event_id"
36
-    t.datetime "created_at",                                               null: false
37
-    t.datetime "updated_at",                                               null: false
38
-    t.text     "memory",                limit: 2147483647
39
+    t.datetime "created_at"
40
+    t.datetime "updated_at"
41
+    t.text     "memory"
39 42
     t.datetime "last_web_request_at"
43
+    t.integer  "keep_events_for",       default: 0,     null: false
40 44
     t.datetime "last_event_at"
41 45
     t.datetime "last_error_log_at"
42
-    t.integer  "keep_events_for",                          default: 0,     null: false
43
-    t.boolean  "propagate_immediately",                    default: false, null: false
44
-    t.boolean  "disabled",                                 default: false, null: false
45
-    t.string   "guid",                                                     null: false
46
+    t.boolean  "propagate_immediately", default: false, null: false
47
+    t.boolean  "disabled",              default: false, null: false
48
+    t.string   "guid",                                  null: false
49
+    t.integer  "service_id"
46 50
   end
47 51
 
48 52
   add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree
@@ -51,17 +55,17 @@ ActiveRecord::Schema.define(version: 20140820003139) do
51 55
   add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree
52 56
 
53 57
   create_table "delayed_jobs", force: true do |t|
54
-    t.integer  "priority",                    default: 0
55
-    t.integer  "attempts",                    default: 0
56
-    t.text     "handler",    limit: 16777215
57
-    t.text     "last_error", limit: 16777215
58
+    t.integer  "priority",   default: 0
59
+    t.integer  "attempts",   default: 0
60
+    t.text     "handler"
61
+    t.text     "last_error"
58 62
     t.datetime "run_at"
59 63
     t.datetime "locked_at"
60 64
     t.datetime "failed_at"
61 65
     t.string   "locked_by"
62 66
     t.string   "queue"
63
-    t.datetime "created_at",                              null: false
64
-    t.datetime "updated_at",                              null: false
67
+    t.datetime "created_at"
68
+    t.datetime "updated_at"
65 69
   end
66 70
 
67 71
   add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree
@@ -69,11 +73,11 @@ ActiveRecord::Schema.define(version: 20140820003139) do
69 73
   create_table "events", force: true do |t|
70 74
     t.integer  "user_id"
71 75
     t.integer  "agent_id"
72
-    t.decimal  "lat",                           precision: 15, scale: 10
73
-    t.decimal  "lng",                           precision: 15, scale: 10
74
-    t.text     "payload",    limit: 2147483647
75
-    t.datetime "created_at",                                              null: false
76
-    t.datetime "updated_at",                                              null: false
76
+    t.decimal  "lat",        precision: 15, scale: 10
77
+    t.decimal  "lng",        precision: 15, scale: 10
78
+    t.text     "payload"
79
+    t.datetime "created_at"
80
+    t.datetime "updated_at"
77 81
     t.datetime "expires_at"
78 82
   end
79 83
 
@@ -84,8 +88,8 @@ ActiveRecord::Schema.define(version: 20140820003139) do
84 88
   create_table "links", force: true do |t|
85 89
     t.integer  "source_id"
86 90
     t.integer  "receiver_id"
87
-    t.datetime "created_at",                       null: false
88
-    t.datetime "updated_at",                       null: false
91
+    t.datetime "created_at"
92
+    t.datetime "updated_at"
89 93
     t.integer  "event_id_at_creation", default: 0, null: false
90 94
   end
91 95
 
@@ -103,13 +107,13 @@ ActiveRecord::Schema.define(version: 20140820003139) do
103 107
   add_index "scenario_memberships", ["scenario_id"], name: "index_scenario_memberships_on_scenario_id", using: :btree
104 108
 
105 109
   create_table "scenarios", force: true do |t|
106
-    t.string   "name",                        null: false
107
-    t.integer  "user_id",                     null: false
110
+    t.string   "name",                         null: false
111
+    t.integer  "user_id",                      null: false
108 112
     t.datetime "created_at"
109 113
     t.datetime "updated_at"
110 114
     t.text     "description"
111
-    t.boolean  "public",      default: false, null: false
112
-    t.string   "guid",                        null: false
115
+    t.boolean  "public",       default: false, null: false
116
+    t.string   "guid",                         null: false
113 117
     t.string   "source_url"
114 118
     t.string   "tag_bg_color"
115 119
     t.string   "tag_fg_color"
@@ -117,12 +121,31 @@ ActiveRecord::Schema.define(version: 20140820003139) do
117 121
 
118 122
   add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree
119 123
 
124
+  create_table "services", force: true do |t|
125
+    t.integer  "user_id",                       null: false
126
+    t.string   "provider",                      null: false
127
+    t.string   "name",                          null: false
128
+    t.text     "token",                         null: false
129
+    t.text     "secret"
130
+    t.text     "refresh_token"
131
+    t.datetime "expires_at"
132
+    t.boolean  "global",        default: false
133
+    t.text     "options"
134
+    t.datetime "created_at"
135
+    t.datetime "updated_at"
136
+    t.string   "uid"
137
+  end
138
+
139
+  add_index "services", ["provider"], name: "index_services_on_provider", using: :btree
140
+  add_index "services", ["uid"], name: "index_services_on_uid", using: :btree
141
+  add_index "services", ["user_id", "global"], name: "index_services_on_user_id_and_global", using: :btree
142
+
120 143
   create_table "user_credentials", force: true do |t|
121 144
     t.integer  "user_id",                           null: false
122 145
     t.string   "credential_name",                   null: false
123 146
     t.text     "credential_value",                  null: false
124
-    t.datetime "created_at",                        null: false
125
-    t.datetime "updated_at",                        null: false
147
+    t.datetime "created_at"
148
+    t.datetime "updated_at"
126 149
     t.string   "mode",             default: "text", null: false
127 150
   end
128 151
 
@@ -139,8 +162,8 @@ ActiveRecord::Schema.define(version: 20140820003139) do
139 162
     t.datetime "last_sign_in_at"
140 163
     t.string   "current_sign_in_ip"
141 164
     t.string   "last_sign_in_ip"
142
-    t.datetime "created_at",                             null: false
143
-    t.datetime "updated_at",                             null: false
165
+    t.datetime "created_at"
166
+    t.datetime "updated_at"
144 167
     t.boolean  "admin",                  default: false, null: false
145 168
     t.integer  "failed_attempts",        default: 0
146 169
     t.string   "unlock_token"

+ 58 - 0
spec/controllers/services_controller_spec.rb

@@ -0,0 +1,58 @@
1
+require 'spec_helper'
2
+
3
+describe ServicesController do
4
+  before do
5
+    sign_in users(:bob)
6
+    OmniAuth.config.test_mode = true
7
+    request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
8
+  end
9
+
10
+  describe "GET index" do
11
+    it "only returns sevices of the current user" do
12
+      get :index
13
+      assigns(:services).all? {|i| i.user.should == users(:bob) }.should == true
14
+    end
15
+  end
16
+
17
+  describe "POST toggle_availability" do
18
+    it "should work for service of the user" do
19
+      post :toggle_availability, :id => services(:generic).to_param
20
+      assigns(:service).should eq(services(:generic))
21
+      redirect_to(services_path)
22
+    end
23
+
24
+    it "should not work for a service of another user" do
25
+      lambda {
26
+        post :toggle_availability, :id => services(:global).to_param
27
+      }.should raise_error(ActiveRecord::RecordNotFound)
28
+    end
29
+  end
30
+
31
+  describe "DELETE destroy" do
32
+    it "destroys only services owned by the current user" do
33
+      expect {
34
+        delete :destroy, :id => services(:generic).to_param
35
+      }.to change(Service, :count).by(-1)
36
+
37
+      lambda {
38
+        delete :destroy, :id => services(:global).to_param
39
+      }.should raise_error(ActiveRecord::RecordNotFound)
40
+    end
41
+  end
42
+
43
+  describe "accepting a callback url" do
44
+    it "should update the user's credentials" do
45
+      expect {
46
+        get :callback, provider: 'twitter'
47
+      }.to change { users(:bob).services.count }.by(1)
48
+    end
49
+
50
+    it "should work with an unknown provider (for now)" do
51
+      request.env["omniauth.auth"]['provider'] = 'unknown'
52
+      expect {
53
+        get :callback, provider: 'unknown'
54
+      }.to change { users(:bob).services.count }.by(1)
55
+      users(:bob).services.first.provider.should == 'unknown'
56
+    end
57
+  end
58
+end

+ 43 - 0
spec/data_fixtures/services/37signals.json

@@ -0,0 +1,43 @@
1
+{
2
+  "provider": "37signals",
3
+  "uid": 12345,
4
+  "info": {
5
+    "email": "basecamp@none.de",
6
+    "first_name": "Dominik",
7
+    "last_name": "Sander",
8
+    "name": "Dominik Sander"
9
+  },
10
+  "credentials": {
11
+    "token": "abcde",
12
+    "refresh_token": "fghrefresh",
13
+    "expires_at": 1401554352,
14
+    "expires": true
15
+  },
16
+  "extra": {
17
+    "accounts": [
18
+      {
19
+        "product": "bcx",
20
+        "name": "Dominik Sander's Basecamp",
21
+        "id": 12345,
22
+        "href": "https://basecamp.com/12345/api/v1"
23
+      }
24
+    ],
25
+    "raw_info": {
26
+      "expires_at": "2014-05-31T16:39:12Z",
27
+      "identity": {
28
+        "first_name": "Dominik",
29
+        "last_name": "Sander",
30
+        "email_address": "basecamp@none.de",
31
+        "id": 12345
32
+      },
33
+      "accounts": [
34
+        {
35
+          "product": "bcx",
36
+          "name": "Dominik Sander's Basecamp",
37
+          "id": 12345,
38
+          "href": "https://basecamp.com/12345/api/v1"
39
+        }
40
+      ]
41
+    }
42
+  }
43
+}

+ 52 - 0
spec/data_fixtures/services/github.json

@@ -0,0 +1,52 @@
1
+{
2
+  "provider": "github",
3
+  "uid": "12345",
4
+  "info": {
5
+    "nickname": "dsander",
6
+    "email": null,
7
+    "name": "Dominik Sander",
8
+    "image": "https://avatars.githubusercontent.com/u/12345?",
9
+    "urls": {
10
+      "GitHub": "https://github.com/dsander",
11
+      "Blog": "http://www.dsander.de"
12
+    }
13
+  },
14
+  "credentials": {
15
+    "token": "agithubtoken",
16
+    "expires": false
17
+  },
18
+  "extra": {
19
+    "raw_info": {
20
+      "login": "dsander",
21
+      "id": 12345,
22
+      "avatar_url": "https://avatars.githubusercontent.com/u/12345?",
23
+      "gravatar_id": "fsdfsdf",
24
+      "url": "https://api.github.com/users/dsander",
25
+      "html_url": "https://github.com/dsander",
26
+      "followers_url": "https://api.github.com/users/dsander/followers",
27
+      "following_url": "https://api.github.com/users/dsander/following{/other_user}",
28
+      "gists_url": "https://api.github.com/users/dsander/gists{/gist_id}",
29
+      "starred_url": "https://api.github.com/users/dsander/starred{/owner}{/repo}",
30
+      "subscriptions_url": "https://api.github.com/users/dsander/subscriptions",
31
+      "organizations_url": "https://api.github.com/users/dsander/orgs",
32
+      "repos_url": "https://api.github.com/users/dsander/repos",
33
+      "events_url": "https://api.github.com/users/dsander/events{/privacy}",
34
+      "received_events_url": "https://api.github.com/users/dsander/received_events",
35
+      "type": "User",
36
+      "site_admin": false,
37
+      "name": "Dominik Sander",
38
+      "company": null,
39
+      "blog": "http://www.url.de",
40
+      "location": null,
41
+      "email": null,
42
+      "hireable": false,
43
+      "bio": null,
44
+      "public_repos": 29,
45
+      "public_gists": 2,
46
+      "followers": 21,
47
+      "following": 9,
48
+      "created_at": "2008-08-17T18:17:50Z",
49
+      "updated_at": "2014-05-19T09:30:08Z"
50
+    }
51
+  }
52
+}

+ 66 - 0
spec/data_fixtures/services/twitter.json

@@ -0,0 +1,66 @@
1
+{
2
+  "provider": "twitter",
3
+  "uid": "123456",
4
+  "info": {
5
+    "nickname": "johnqpublic",
6
+    "name": "John Q Public",
7
+    "location": "Anytown, USA",
8
+    "image": "http://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png",
9
+    "description": "a very normal guy.",
10
+    "urls": {
11
+      "Website": null,
12
+      "Twitter": "https://twitter.com/johnqpublic"
13
+    }
14
+  },
15
+  "credentials": {
16
+    "token": "a1b2c3d4...",
17
+    "secret": "abcdef1234"
18
+  },
19
+  "extra": {
20
+    "access_token": "",
21
+    "raw_info": {
22
+      "name": "John Q Public",
23
+      "listed_count": 0,
24
+      "profile_sidebar_border_color": "181A1E",
25
+      "url": null,
26
+      "lang": "en",
27
+      "statuses_count": 129,
28
+      "profile_image_url": "http://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png",
29
+      "profile_background_image_url_https": "https://twimg0-a.akamaihd.net/profile_background_images/229171796/pattern_036.gif",
30
+      "location": "Anytown, USA",
31
+      "time_zone": "Chicago",
32
+      "follow_request_sent": false,
33
+      "id": 123456,
34
+      "profile_background_tile": true,
35
+      "profile_sidebar_fill_color": "666666",
36
+      "followers_count": 1,
37
+      "default_profile_image": false,
38
+      "screen_name": "",
39
+      "following": false,
40
+      "utc_offset": -3600,
41
+      "verified": false,
42
+      "favourites_count": 0,
43
+      "profile_background_color": "1A1B1F",
44
+      "is_translator": false,
45
+      "friends_count": 1,
46
+      "notifications": false,
47
+      "geo_enabled": true,
48
+      "profile_background_image_url": "http://twimg0-a.akamaihd.net/profile_background_images/229171796/pattern_036.gif",
49
+      "protected": false,
50
+      "description": "a very normal guy.",
51
+      "profile_link_color": "2FC2EF",
52
+      "created_at": "Thu Jul 4 00:00:00 +0000 2013",
53
+      "id_str": "123456",
54
+      "profile_image_url_https": "https://si0.twimg.com/sticky/default_profile_images/default_profile_2_normal.png",
55
+      "default_profile": false,
56
+      "profile_use_background_image": false,
57
+      "entities": {
58
+        "description": {
59
+          "urls": []
60
+        }
61
+      },
62
+      "profile_text_color": "666666",
63
+      "contributors_enabled": false
64
+    }
65
+  }
66
+}

+ 5 - 0
spec/env.test

@@ -0,0 +1,5 @@
1
+APP_SECRET_TOKEN=notarealappsecrettoken
2
+TWITTER_OAUTH_KEY=twitteroauthkey
3
+TWITTER_OAUTH_SECRET=twitteroauthsecret
4
+THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY
5
+THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET

+ 12 - 0
spec/fixtures/agents.yml

@@ -109,3 +109,15 @@ bob_manual_event_agent:
109 109
   user: bob
110 110
   name: "Bob's event testing agent"
111 111
   guid: <%= SecureRandom.hex %>
112
+
113
+bob_basecamp_agent:
114
+  type: Agents::BasecampAgent
115
+  user: bob
116
+  service: generic
117
+  guid: <%= SecureRandom.hex %>
118
+
119
+jane_basecamp_agent:
120
+  type: Agents::BasecampAgent
121
+  user: jane
122
+  service: generic
123
+  guid: <%= SecureRandom.hex %>

+ 17 - 0
spec/fixtures/services.yml

@@ -0,0 +1,17 @@
1
+generic:
2
+  token: 1234token
3
+  secret: 56789secret
4
+  refresh_token: refresh12345
5
+  provider: testprovider
6
+  name: test
7
+  expires_at: <%= Time.parse("2015-01-01 00:00:00") %>
8
+  options: <%= { user_id: 12345 }.to_yaml.inspect %>
9
+  user: bob
10
+global:
11
+  token: 1234token
12
+  provider: testprovider
13
+  name: test
14
+  expires_at: <%= Time.parse("2015-01-01 00:00:00") %>
15
+  options: <%= { user_id: 12345 }.to_yaml.inspect %>
16
+  user: jane
17
+  global: true

+ 11 - 26
spec/models/agents/basecamp_agent_spec.rb

@@ -1,17 +1,16 @@
1 1
 require 'spec_helper'
2
+require 'models/concerns/oauthable'
2 3
 
3 4
 describe Agents::BasecampAgent do
5
+  it_behaves_like Oauthable
6
+
4 7
   before(:each) do
5 8
     stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
6
-    stub_request(:get, /Z$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
7
-    @valid_params = {
8
-                      :username   => "user",
9
-                      :password   => "pass",
10
-                      :user_id    => 12345,
11
-                      :project_id => 6789,
12
-                    }
9
+    stub_request(:get, /02:00$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
10
+    @valid_params = { :project_id => 6789 }
13 11
 
14 12
     @checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params)
13
+    @checker.service = services(:generic)
15 14
     @checker.user = users(:jane)
16 15
     @checker.save!
17 16
   end
@@ -21,21 +20,6 @@ describe Agents::BasecampAgent do
21 20
       @checker.should be_valid
22 21
     end
23 22
 
24
-    it "should require the basecamp username" do
25
-      @checker.options['username'] = nil
26
-      @checker.should_not be_valid
27
-    end
28
-
29
-    it "should require the basecamp password" do
30
-      @checker.options['password'] = nil
31
-      @checker.should_not be_valid
32
-    end
33
-
34
-    it "should require the basecamp user_id" do
35
-      @checker.options['user_id'] = nil
36
-      @checker.should_not be_valid
37
-    end
38
-
39 23
     it "should require the basecamp project_id" do
40 24
       @checker.options['project_id'] = nil
41 25
       @checker.should_not be_valid
@@ -45,7 +29,7 @@ describe Agents::BasecampAgent do
45 29
 
46 30
   describe "helpers" do
47 31
     it "should generate a correct request options hash" do
48
-      @checker.send(:request_options).should == {:basic_auth=>{:username=>"user", :password=>"pass"}, :headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)"}}
32
+      @checker.send(:request_options).should == {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => 'Bearer "1234token"'}}
49 33
     end
50 34
 
51 35
     it "should generate the currect request url" do
@@ -59,7 +43,7 @@ describe Agents::BasecampAgent do
59 43
 
60 44
     it "should provide the since attribute after the first run" do
61 45
       time = (Time.now-1.minute).iso8601
62
-      @checker.memory[:last_run] = time
46
+      @checker.memory[:last_event] = time
63 47
       @checker.save
64 48
       @checker.reload.send(:query_parameters).should == {:query => {:since => time}}
65 49
     end
@@ -67,9 +51,10 @@ describe Agents::BasecampAgent do
67 51
   describe "#check" do
68 52
     it "should not emit events on its first run" do
69 53
       expect { @checker.check }.to change { Event.count }.by(0)
54
+      expect(@checker.memory[:last_event]).to eq '2014-04-17T10:25:31.000+02:00'
70 55
     end
71 56
     it "should check that initial run creates an event" do
72
-      @checker.last_check_at = Time.now - 1.minute
57
+      @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00'
73 58
       expect { @checker.check }.to change { Event.count }.by(1)
74 59
     end
75 60
   end
@@ -77,7 +62,7 @@ describe Agents::BasecampAgent do
77 62
   describe "#working?" do
78 63
     it "it is working when at least one event was emited" do
79 64
       @checker.should_not be_working
80
-      @checker.last_check_at = Time.now - 1.minute
65
+      @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00'
81 66
       @checker.check
82 67
       @checker.reload.should be_working
83 68
     end

+ 1 - 0
spec/models/agents/twitter_publish_agent_spec.rb

@@ -13,6 +13,7 @@ describe Agents::TwitterPublishAgent do
13 13
     }
14 14
 
15 15
     @checker = Agents::TwitterPublishAgent.new(:name => "HuginnBot", :options => @opts)
16
+    @checker.service = services(:generic)
16 17
     @checker.user = users(:bob)
17 18
     @checker.save!
18 19
 

+ 1 - 0
spec/models/agents/twitter_stream_agent_spec.rb

@@ -13,6 +13,7 @@ describe Agents::TwitterStreamAgent do
13 13
     }
14 14
 
15 15
     @agent = Agents::TwitterStreamAgent.new(:name => "HuginnBot", :options => @opts)
16
+    @agent.service = services(:generic)
16 17
     @agent.user = users(:bob)
17 18
     @agent.save!
18 19
   end

+ 2 - 0
spec/models/agents/twitter_user_agent_spec.rb

@@ -16,6 +16,7 @@ describe Agents::TwitterUserAgent do
16 16
     }
17 17
 
18 18
     @checker = Agents::TwitterUserAgent.new(:name => "tectonic", :options => @opts)
19
+    @checker.service = services(:generic)
19 20
     @checker.user = users(:bob)
20 21
     @checker.save!
21 22
   end
@@ -31,6 +32,7 @@ describe Agents::TwitterUserAgent do
31 32
       opts = @opts.merge({ :starting_at => "Jan 01 00:00:01 +0000 2999", })
32 33
 
33 34
       checker = Agents::TwitterUserAgent.new(:name => "tectonic", :options => opts)
35
+      checker.service = services(:generic)
34 36
       checker.user = users(:bob)
35 37
       checker.save!
36 38
 

+ 29 - 0
spec/models/concerns/oauthable.rb

@@ -0,0 +1,29 @@
1
+require 'spec_helper'
2
+
3
+module Agents
4
+  class OauthableTestAgent < Agent
5
+    include Oauthable
6
+  end
7
+end
8
+
9
+shared_examples_for Oauthable do
10
+  before(:each) do
11
+    @agent = described_class.new(:name => "somename")
12
+    @agent.user = users(:jane)
13
+  end
14
+
15
+  it "should be oauthable" do
16
+    @agent.oauthable?.should == true
17
+  end
18
+
19
+  describe "valid_services_for" do
20
+    it "should return all available services without specifying valid_oauth_providers" do
21
+      @agent = Agents::OauthableTestAgent.new
22
+      @agent.valid_services_for(users(:bob)).collect(&:id).sort.should == [services(:generic), services(:global)].collect(&:id).sort
23
+    end
24
+
25
+    it "should filter the services based on the agent defaults" do
26
+      @agent.valid_services_for(users(:bob)).to_a.should == Service.where(provider: @agent.valid_oauth_providers)
27
+    end
28
+  end
29
+end

+ 55 - 0
spec/models/scenario_import_spec.rb

@@ -47,6 +47,18 @@ describe ScenarioImport do
47 47
       :options => trigger_agent_options
48 48
     }
49 49
   end
50
+  let(:valid_parsed_basecamp_agent_data) do
51
+    {
52
+      :type => "Agents::BasecampAgent",
53
+      :name => "Basecamp test",
54
+      :schedule => "every_2m",
55
+      :keep_events_for => 0,
56
+      :propagate_immediately => true,
57
+      :disabled => false,
58
+      :guid => "a-basecamp-agent",
59
+      :options => {project_id: 12345}
60
+    }
61
+  end
50 62
   let(:valid_parsed_data) do
51 63
     {
52 64
       :name => name,
@@ -415,5 +427,48 @@ describe ScenarioImport do
415 427
         end
416 428
       end
417 429
     end
430
+
431
+    context "agents which require a service" do
432
+      let(:valid_parsed_services) do
433
+        data = valid_parsed_data
434
+        data[:agents] = [valid_parsed_basecamp_agent_data,
435
+                         valid_parsed_trigger_agent_data]
436
+        data
437
+      end
438
+
439
+      let(:valid_parsed_services_data) { valid_parsed_services.to_json }
440
+
441
+      let(:services_scenario_import) {
442
+        _import = ScenarioImport.new(:data => valid_parsed_services_data)
443
+        _import.set_user users(:bob)
444
+        _import
445
+      }
446
+
447
+      describe "#generate_diff" do
448
+        it "should check if the agent requires a service" do
449
+          agent_diffs = services_scenario_import.agent_diffs
450
+          basecamp_agent_diff = agent_diffs[0]
451
+          basecamp_agent_diff.requires_service?.should == true
452
+        end
453
+
454
+        it "should add an error when no service is selected" do
455
+          services_scenario_import.import.should == false
456
+          services_scenario_import.errors[:base].length.should == 1
457
+        end
458
+      end
459
+
460
+      describe "#import" do
461
+        it "should import" do
462
+          services_scenario_import.merges = {
463
+            "0" => {
464
+              "service_id" => "0",
465
+            }
466
+          }
467
+          lambda {
468
+            services_scenario_import.import.should == true
469
+          }.should change { users(:bob).agents.count }.by(2)
470
+        end
471
+      end
472
+    end
418 473
   end
419 474
 end

+ 129 - 0
spec/models/service_spec.rb

@@ -0,0 +1,129 @@
1
+require 'spec_helper'
2
+
3
+describe Service do
4
+  before(:each) do
5
+    @user = users(:bob)
6
+  end
7
+
8
+  describe "#toggle_availability!" do
9
+    it "should toggle the global flag" do
10
+      @service = services(:generic)
11
+      @service.global.should == false
12
+      @service.toggle_availability!
13
+      @service.global.should == true
14
+      @service.toggle_availability!
15
+      @service.global.should == false
16
+    end
17
+
18
+    it "disconnects agents and disables them if the previously global service is made private again", focus: true do
19
+      agent = agents(:bob_basecamp_agent)
20
+      jane_agent = agents(:jane_basecamp_agent)
21
+
22
+      service = agent.service
23
+      service.toggle_availability!
24
+      service.agents.length.should == 2
25
+
26
+      service.toggle_availability!
27
+      jane_agent.reload
28
+      jane_agent.service_id.should be_nil
29
+      jane_agent.disabled.should be true
30
+
31
+      service.reload
32
+      service.agents.length.should == 1
33
+    end
34
+  end
35
+
36
+  it "disables all agents before beeing destroyed" do
37
+    agent = agents(:bob_basecamp_agent)
38
+    service = agent.service
39
+    service.destroy
40
+    agent.reload
41
+    agent.service_id.should be_nil
42
+    agent.disabled.should be true
43
+  end
44
+
45
+  describe "preparing for a request" do
46
+    before(:each) do
47
+      @service = services(:generic)
48
+    end
49
+
50
+    it "should not update the token if the token never expires" do
51
+      @service.expires_at = nil
52
+      @service.prepare_request.should == nil
53
+    end
54
+
55
+    it "should not update the token if the token is still valid" do
56
+      @service.expires_at = Time.now + 1.hour
57
+      @service.prepare_request.should == nil
58
+    end
59
+
60
+    it "should call refresh_token! if the token expired" do
61
+      stub(@service).refresh_token! { @service }
62
+      @service.expires_at = Time.now - 1.hour
63
+      @service.prepare_request.should == @service
64
+    end
65
+  end
66
+
67
+  describe "updating the access token" do
68
+    before(:each) do
69
+      @service = services(:generic)
70
+    end
71
+
72
+    it "should return the correct endpoint" do
73
+      @service.provider = '37signals'
74
+      @service.send(:endpoint).to_s.should == "https://launchpad.37signals.com/authorization/token"
75
+    end
76
+
77
+    it "should update the token" do
78
+      stub_request(:post, "https://launchpad.37signals.com/authorization/token?client_id=TESTKEY&client_secret=TESTSECRET&refresh_token=refreshtokentest&type=refresh").
79
+        to_return(:status => 200, :body => '{"expires_in":1209600,"access_token": "NEWTOKEN"}', :headers => {})
80
+      @service.provider = '37signals'
81
+      @service.refresh_token = 'refreshtokentest'
82
+      @service.refresh_token!
83
+      @service.token.should == 'NEWTOKEN'
84
+    end
85
+  end
86
+
87
+  describe "creating services via omniauth" do
88
+    it "should work with twitter services" do
89
+      twitter = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
90
+      expect {
91
+        service = @user.services.initialize_or_update_via_omniauth(twitter)
92
+        service.save!
93
+      }.to change { @user.services.count }.by(1)
94
+      service = @user.services.first
95
+      service.name.should == 'johnqpublic'
96
+      service.uid.should == '123456'
97
+      service.provider.should == 'twitter'
98
+      service.token.should == 'a1b2c3d4...'
99
+      service.secret.should == 'abcdef1234'
100
+    end
101
+    it "should work with 37signals services" do
102
+      signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/37signals.json')))
103
+      expect {
104
+        service = @user.services.initialize_or_update_via_omniauth(signals)
105
+        service.save!
106
+      }.to change { @user.services.count }.by(1)
107
+      service = @user.services.first
108
+      service.provider.should == '37signals'
109
+      service.name.should == 'Dominik Sander'
110
+      service.token.should == 'abcde'
111
+      service.uid.should == '12345'
112
+      service.refresh_token.should == 'fghrefresh'
113
+      service.options[:user_id].should == 12345
114
+      service.expires_at = Time.at(1401554352)
115
+    end
116
+    it "should work with github services" do
117
+      signals = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/github.json')))
118
+      expect {
119
+        service = @user.services.initialize_or_update_via_omniauth(signals)
120
+        service.save!
121
+      }.to change { @user.services.count }.by(1)
122
+      service = @user.services.first
123
+      service.provider.should == 'github'
124
+      service.name.should == 'dsander'
125
+      service.uid.should == '12345'
126
+      service.token.should == 'agithubtoken'
127
+    end
128
+  end
129
+end

+ 7 - 2
spec/spec_helper.rb

@@ -1,4 +1,3 @@
1
-# This file is copied to spec/ when you run 'rails generate rspec:install'
2 1
 ENV["RAILS_ENV"] ||= 'test'
3 2
 
4 3
 if ENV['COVERAGE']
@@ -9,6 +8,10 @@ else
9 8
   Coveralls.wear!('rails')
10 9
 end
11 10
 
11
+# Required ENV variables that are normally set in .env are setup here for the test environment.
12
+require 'dotenv'
13
+Dotenv.overload File.join(File.dirname(__FILE__), "env.test")
14
+
12 15
 require File.expand_path("../../config/environment", __FILE__)
13 16
 require 'rspec/rails'
14 17
 require 'rspec/autorun'
@@ -19,7 +22,9 @@ WebMock.disable_net_connect!
19 22
 
20 23
 # Requires supporting ruby files with custom matchers and macros, etc,
21 24
 # in spec/support/ and its subdirectories.
22
-Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
25
+Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
26
+
27
+ActiveRecord::Migration.maintain_test_schema!
23 28
 
24 29
 RSpec.configure do |config|
25 30
   config.mock_with :rr